// Copyright 2019 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package helmreconciler

import (
	"context"

	"istio.io/pkg/log"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// Prune removes any resources not specified in manifests generated by HelmReconciler h. If all is set to true, this
// function prunes all resources.
func (h *HelmReconciler) Prune(all bool) error {
	allErrors := []error{}
	namespacedResourceMap, nonNamespacedResourceMap, _ := h.customizer.PruningDetails().GetResourceTypes()
	targetNamespace := h.customizer.Input().GetTargetNamespace()
	err := h.PruneResources(namespacedResourceMap, all, targetNamespace)
	if err != nil {
		allErrors = append(allErrors, err)
	}
	err = h.PruneResources(nonNamespacedResourceMap, all, "")
	if err != nil {
		allErrors = append(allErrors, err)
	}
	return utilerrors.NewAggregate(allErrors)
}

// Prune removes any resources not specified resourceMap. If all is set to true, it prunes all
// resources.
func (h *HelmReconciler) PruneResources(resourceMap map[schema.GroupVersionKind]bool, all bool, namespace string) error {
	allErrors := []error{}
	ownerLabels := h.customizer.PruningDetails().GetOwnerLabels()
	ownerAnnotations := h.customizer.PruningDetails().GetOwnerAnnotations()
	for gvk, exists := range resourceMap {
		if exists {
			objects := &unstructured.UnstructuredList{}
			objects.SetGroupVersionKind(gvk)
			err := h.client.List(context.TODO(), objects, client.MatchingLabels(ownerLabels), client.InNamespace(namespace))
			if err != nil {
				// we only want to retrieve resources clusters
				log.Warnf("retrieving resources to prune type %s: %s not found", gvk.String(), err)
				continue
			}
		objectLoop:
			for _, object := range objects.Items {
				annotations := object.GetAnnotations()
				for ownerKey, ownerValue := range ownerAnnotations {
					// we only want to delete objects that contain the annotations
					// if we're not pruning all objects, we only want to prune those whose annotation value does not match what is expected
					if value, ok := annotations[ownerKey]; !ok || (!all && value == ownerValue) {
						continue objectLoop
					}
				}
				err = h.client.Delete(context.TODO(), &object, client.PropagationPolicy(metav1.DeletePropagationBackground))
				if err == nil {
					if listenerErr := h.customizer.Listener().ResourceDeleted(&object); listenerErr != nil {
						log.Errorf("error calling listener: %s", err)
					}
				} else {
					if listenerErr := h.customizer.Listener().ResourceError(&object, err); listenerErr != nil {
						log.Errorf("error calling listener: %s", err)
					}
					allErrors = append(allErrors, err)
				}
			}
		}
	}
	return utilerrors.NewAggregate(allErrors)
}
